package com.googlecode.alfrescocachingserver.services.impl;

import java.io.ByteArrayInputStream;
import java.io.InputStream;
import java.io.UnsupportedEncodingException;
import java.util.Map.Entry;
import java.util.Set;
import java.util.TreeSet;

import net.sf.ehcache.Cache;
import net.sf.ehcache.CacheManager;
import net.sf.ehcache.Ehcache;
import net.sf.ehcache.Element;
import net.sf.ehcache.config.CacheConfiguration;
import net.sf.ehcache.constructs.blocking.SelfPopulatingCache;
import net.sf.ehcache.store.MemoryStoreEvictionPolicy;

import org.apache.commons.configuration.Configuration;
import org.apache.commons.io.IOUtils;
import org.apache.commons.lang3.StringUtils;
import org.dom4j.DocumentException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationListener;
import org.springframework.context.event.ContextRefreshedEvent;
import org.springframework.extensions.webscripts.DeclarativeRegistry;
import org.springframework.extensions.webscripts.Description;
import org.springframework.extensions.webscripts.DescriptionImpl;
import org.springframework.extensions.webscripts.Match;
import org.springframework.extensions.webscripts.UriIndex;
import org.springframework.http.HttpMethod;

import com.googlecode.alfrescocachingserver.WebScriptCacheEntryFactory;
import com.googlecode.alfrescocachingserver.exceptions.ServiceException;
import com.googlecode.alfrescocachingserver.extractor.CacheIdExtractor;
import com.googlecode.alfrescocachingserver.model.CacheEntry;
import com.googlecode.alfrescocachingserver.model.CacheWebScript;
import com.googlecode.alfrescocachingserver.model.WebScriptCacheKey;
import com.googlecode.alfrescocachingserver.model.WebScriptResponse;
import com.googlecode.alfrescocachingserver.model.WebScriptUri;
import com.googlecode.alfrescocachingserver.model.schemas.CacheConfig;
import com.googlecode.alfrescocachingserver.model.schemas.ServerCacheConfig;
import com.googlecode.alfrescocachingserver.model.schemas.ServerCacheConfigs;
import com.googlecode.alfrescocachingserver.model.schemas.WebScriptUriTemplate;
import com.googlecode.alfrescocachingserver.model.schemas.util.SchemaUtils;
import com.googlecode.alfrescocachingserver.services.CacheKeysMappingService;
import com.googlecode.alfrescocachingserver.services.CacheService;
import com.googlecode.alfrescocachingserver.services.WebScriptConnectorService;
import com.googlecode.alfrescocachingserver.util.GlobalConstants;

public class CacheServiceImpl implements CacheService, ApplicationListener<ContextRefreshedEvent> 
{
    private static final Logger LOG = LoggerFactory.getLogger(CacheServiceImpl.class);
    
    private static final WebScriptUri URI_SERVER_CACHE_CONFIG = new WebScriptUri(HttpMethod.GET.name(), "/alfresco/index/server-cache");

    protected DeclarativeRegistry uriRegistry;
    protected UriIndex uriIndex;

    protected CacheManager cacheManager;

    protected WebScriptConnectorService webScriptConnectorService;
    protected CacheKeysMappingService cacheKeysMappingService;

    protected Configuration configuration;

    private static boolean initialized = false;
    
    public void setCacheManager(CacheManager cacheManager)
    {
        this.cacheManager = cacheManager;
    }

    public void setConfiguration(Configuration configuration)
    {
        this.configuration = configuration;
    }

    public void setWebScriptConnectorService(WebScriptConnectorService webScriptConnectorService)
    {
        this.webScriptConnectorService = webScriptConnectorService;
    }
    
    public void setUriRegistry(DeclarativeRegistry uriRegistry)
    {
        this.uriRegistry = uriRegistry;
    }

    public void setUriIndex(UriIndex uriIndex)
    {
        this.uriIndex = uriIndex;
    }

    public void setCacheKeysMappingService(CacheKeysMappingService cacheKeysMappingService)
    {
        this.cacheKeysMappingService = cacheKeysMappingService;
    }

    public void initialize(ApplicationContext applicationContext)
    {
        if (initialized)
        {
            return;
        }
        
        LOG.info("Initializing Caches");

        try
        {
            WebScriptResponse webScriptResponse = webScriptConnectorService.processRequest(URI_SERVER_CACHE_CONFIG, null, null);
            
            // TODO:  capture exception if failure during server cache config retrieval.  If failure, read latest
            //        config from disk and use it to create the caches.  If retrieval successful and successful cache creation, store config on disk

            if (LOG.isDebugEnabled())
            {
                LOG.debug("response ={}{}", GlobalConstants.LINE_SEPARATOR, new String(webScriptResponse.getResponseBytes()));
            }
            
            InputStream inputStream = new ByteArrayInputStream(webScriptResponse.getResponseBytes());
            
            ServerCacheConfigs serverCacheConfigs = SchemaUtils.getInstance().getServerCacheConfigs(inputStream);

            for (ServerCacheConfig serverCacheConfig : serverCacheConfigs.getServerCacheConfigs())
            {
                LOG.debug("shortName = {}", serverCacheConfig.getShortname());
                LOG.debug("method = {}", serverCacheConfig.getMethod());
                
                if (serverCacheConfig.getCacheConfig() != null && serverCacheConfig.getUriTemplates() != null)
                {
                    createCache(serverCacheConfig.getCacheConfig());

                    String cacheName = serverCacheConfig.getCacheConfig().getName();
                    
                    // TODO:  update logic to get the webscript description
                    Description webScriptDescription = getWebscriptDescription(serverCacheConfig.getMethod(), cacheName);
                    
//                    // TODO:  update to use configuration and remove hard coded value
//                    String cacheIdExtractorBeanName = "uriTemplateIdExtractor";  // serverCacheConfig.getCacheConfig().getCacheIdExtractorBeanName();
//                    
//                    // TODO:  update logic to allow multiple cacheIdExtractors per URI
//                    CacheIdExtractor cacheIdExtractor = null;
//                    String[] extractorParameters = null;
//                    if (cacheIdExtractorBeanName != null)
//                    {
//                        cacheIdExtractor = (CacheIdExtractor)applicationContext.getBean(cacheIdExtractorBeanName);
//                        
//                        // TODO:  update to use configuration and remove hard coded value
//                        extractorParameters = new String[] {"id", "site"}; // serviceCacheConfig.getCacheConfig().getCacheIdExtractorParameters();
//                    }
                    
                    for (WebScriptUriTemplate uriTemplate : serverCacheConfig.getUriTemplates())
                    {
                        CacheIdExtractor uriTemplateExtractor = null;
                        
                        if (StringUtils.isNotBlank(uriTemplate.getCacheIdExtractor()))
                        {
                            String cacheIdExtractorBeanName = uriTemplate.getCacheIdExtractor();
                            
                            CacheIdExtractor cacheIdExtractor = (CacheIdExtractor)applicationContext.getBean(cacheIdExtractorBeanName);
                            
                            String[] extractorParameters = null;
                            if (StringUtils.isNotBlank(uriTemplate.getExtractorParams()))
                            {
                                extractorParameters = uriTemplate.getExtractorParams().split(",");
                            }
                            
                            uriTemplateExtractor = cacheIdExtractor.createInstance(uriTemplate.getUri(), webScriptDescription, extractorParameters);
                        }
                        
                        CacheWebScript tmpCacheWebScript = new CacheWebScript(webScriptDescription, cacheName, uriTemplateExtractor);
                        
                        uriIndex.registerUri(tmpCacheWebScript, uriTemplate.getUri());
                    }
                }
            }
            
            LOG.debug("uriIndex size = {}", uriIndex.getSize());

            initialized = true;
        }
        catch (Exception e)
        {
            LOG.error(e.getMessage(), e);
            throw new ServiceException(e);
        }
    }
    
    @Override
    public void onApplicationEvent(ContextRefreshedEvent contextRefreshedEvent)
    {
        initialize(contextRefreshedEvent.getApplicationContext());
    }
    
    public void createCache(CacheConfig cacheConfig)
    {
        String cacheName = cacheConfig.getName();
        LOG.debug("name = {}", cacheName);
        
        if (StringUtils.isBlank(cacheName))
        {
            throw new ServiceException("Error trying to create cache, cache name required");
        }

        net.sf.ehcache.Cache ehcache = cacheManager.getCache(cacheName);
        
        if (ehcache == null)
        {
            MemoryStoreEvictionPolicy memoryStoreEvictionPolicy = null;
            
            if (StringUtils.isNotBlank(cacheConfig.getMemoryStoreEvictionPolicy()))
            {
                memoryStoreEvictionPolicy = MemoryStoreEvictionPolicy.fromString(cacheConfig.getMemoryStoreEvictionPolicy());
            }
            
            CacheConfiguration ehcacheConfig = new CacheConfiguration();
            ehcacheConfig.setName(cacheName);
            ehcacheConfig.setMaxElementsInMemory(cacheConfig.getMaxElementsInMemory());
            ehcacheConfig.setMemoryStoreEvictionPolicy(cacheConfig.getMemoryStoreEvictionPolicy());
            ehcacheConfig.setOverflowToDisk(cacheConfig.isOverflowToDisk());
            ehcacheConfig.setEternal(cacheConfig.isEternal());
            ehcacheConfig.setTimeToLiveSeconds(cacheConfig.getTimeToLiveSeconds());
            ehcacheConfig.setTimeToIdleSeconds(cacheConfig.getTimeToIdleSeconds());
            if (cacheConfig.isDiskPersistent() != null)
                ehcacheConfig.setDiskPersistent(cacheConfig.isDiskPersistent());
            if (cacheConfig.getDiskExpiryThreadIntervalSeconds() != null)
                ehcacheConfig.setDiskExpiryThreadIntervalSeconds(cacheConfig.getDiskExpiryThreadIntervalSeconds());
            ehcacheConfig.setMaxElementsOnDisk(cacheConfig.getMaxElementsOnDisk());
            ehcacheConfig.setDiskSpoolBufferSizeMB(cacheConfig.getDiskSpoolBufferSizeMB());
            if (cacheConfig.getTransactionalMode() != null)
                ehcacheConfig.setTransactionalMode(cacheConfig.getTransactionalMode());
            if (cacheConfig.getStatistics() != null)
                ehcacheConfig.setStatistics(cacheConfig.getStatistics());
            

            Cache cache = new Cache(ehcacheConfig);
            
//            Cache cache = new Cache(
//                    cacheName,
//                    cacheConfig.getMaxElementsInMemory(),
//                    memoryStoreEvictionPolicy,
//                    cacheConfig.isOverflowToDisk(),
//                    null,   // diskStorePath, EhCache ignores this value
//                    cacheConfig.isEternal(),
//                    cacheConfig.getTimeToLiveSeconds(),
//                    cacheConfig.getTimeToIdleSeconds(),
//                    true,   // cacheConfig.getDiskPersistent(),
//                    120,    // diskExpiryThreadInternvalSeconds
//                    null,   // registeredEventListeners
//                    null,   // bootstrapCacheLoader
//                    cacheConfig.getMaxElementsOnDisk(),
//                    cacheConfig.getDiskSpoolBufferSizeMB());
            
//            PersistenceConfiguration persistenceConfiguration = new PersistenceConfiguration();
//            persistenceConfiguration.setStrategy(Strategy.LOCALRESTARTABLE.name());
//            persistenceConfiguration.setSynchronousWrites(false);
//            
//            cache.getCacheConfiguration().persistence(persistenceConfiguration);
            
            SelfPopulatingCache selfPopulatingCache = new SelfPopulatingCache(
                    cache, new WebScriptCacheEntryFactory(configuration, webScriptConnectorService, cacheKeysMappingService));
            
            CacheManager.getInstance().addCache(selfPopulatingCache);
            LOG.debug("Created Cache:  {}" + cacheName);
        }
    }
    
    public boolean isUriCachable(WebScriptUri webScriptUri)
    {
        boolean isCacheable = getCacheName(webScriptUri) != null;
        
        return isCacheable;
    }
    
    public CacheEntry getCacheEntry(WebScriptCacheKey webScriptCacheKey)
    {
        CacheEntry cacheEntry = null;
        
        WebScriptUri webScriptUri = webScriptCacheKey.getWebScriptUri();

        LOG.debug("webScriptUri = {}", webScriptUri.getUriWithQueryString());
        
        String cacheName = getCacheName(webScriptUri);
        
        if (cacheName != null)
        {
            Ehcache cache = cacheManager.getEhcache(cacheName);
            
            LOG.debug("using cache:  {}", cache.getName());
            
            Element element = cache.get(webScriptCacheKey.toString());
            
            if (element != null)
            {
                LOG.debug("found cache entry:  {}", webScriptCacheKey);
                
                cacheEntry = (CacheEntry)element.getObjectValue();
            }
        }
    
        return cacheEntry;
    }

    public boolean removeCacheEntry(WebScriptCacheKey webScriptCacheKey)
    {
        WebScriptUri webScriptUri = webScriptCacheKey.getWebScriptUri();

        LOG.debug("webScriptUri = {}", webScriptUri.getUriWithQueryString());
        
        String cacheName = getCacheName(webScriptUri);
        
        boolean status = false;
        
        if (cacheName != null)
        {
            Ehcache cache = cacheManager.getEhcache(cacheName);
            
            LOG.debug("using cache:  {}", cache.getName());
            
            status = cache.remove(webScriptCacheKey.toString());
            
            LOG.debug("Removed cache entry:  key = {}, success = {}", webScriptCacheKey, status);
        }
        
        return status;
    }

//    private CacheUriTemplate getCacheUriTemplate(WebScriptUri webScriptUri)
//    {
//        // TODO:  optimize the lookup logic, refer to UriIndex implementations
//        
//        uriRegistry.findWebScript(arg0, arg1).findWebScript("", "");
//        
//        for (CacheUriTemplate cacheUriTemplate : this.uriTemplates)
//        {
//            if (cacheUriTemplate.matches(uri))
//            {
//                return cacheUriTemplate;
//            }
//        }
//        
//        return null;
//    }

    private String getCacheName(WebScriptUri webScriptUri)
    {
        String cacheName = null;
        
        Match match = uriRegistry.findWebScript(webScriptUri.getHttpMethod().name(), webScriptUri.getUri());
        
        if (match != null)
        {
            if (LOG.isDebugEnabled())
            {
                for (Entry<String, String> entry: match.getTemplateVars().entrySet())
                {
                    LOG.debug("template vars:  name = {}, value = {}", entry.getKey(), entry.getValue());
                }
            }

            CacheWebScript webScript = (CacheWebScript)match.getWebScript();
            cacheName = webScript.getCacheName();
        }
        else
        {
            LOG.debug("Matching webScript not found:  {}", webScriptUri.getUriWithQueryString());
        }
        
        return cacheName;
    }

    private Description getWebscriptDescription(String httpMethod, String webScriptId) 
            throws DocumentException, UnsupportedEncodingException
    {
        InputStream inputStream = null;
        
        try
        {
            WebScriptUri webScriptUri = new WebScriptUri(HttpMethod.GET.name(), "/alfresco/description/" + webScriptId);
            WebScriptResponse webScriptResponse = webScriptConnectorService.processRequest(webScriptUri, null, null);
            
            // TODO:  capture exception if failure during server cache config retrieval.  If failure, read latest
            //        config from disk and use it to create the caches.  If retrieval successful and successful cache creation, store config on disk
    
            if (LOG.isDebugEnabled())
            {
                LOG.debug("WebScript description response ={}{}", GlobalConstants.LINE_SEPARATOR, new String(webScriptResponse.getResponseBytes()));
            }
            
            inputStream = new ByteArrayInputStream(webScriptResponse.getResponseBytes());
    
            DescriptionImpl description = new DescriptionImpl();
//            serviceDesc.setScriptPath(scriptPath);
//            serviceDesc.setDescPath(serviceDescPath);
            description.setId(webScriptId);
            description.setMethod(httpMethod);
            
            description.parseDocument(inputStream);
            
            LOG.debug("description = {}", description);
            
            return description;
            
//            CacheWebScript cacheWebScript = new CacheWebScript(description, webScriptId, null);
//        
//            return cacheWebScript;
        }
        finally
        {
            IOUtils.closeQuietly(inputStream);
        }
    }

    @Override
    public Set<String> removeCacheEntries(String id)
    {
        Set<WebScriptCacheKey> cacheKeySet = cacheKeysMappingService.getCacheKeysById(id);
        Set<String> removedCacheKeys = new TreeSet<String>();
        
        
        for (WebScriptCacheKey webScriptCacheKey : cacheKeySet)
        {
            removeCacheEntry(webScriptCacheKey);
            
            removedCacheKeys.add(webScriptCacheKey.toString());
        }
        
        return removedCacheKeys; 
    }
    
    @Override
    public Set<String> flushCache(final String cacheName)
    {
        Set<String> cacheNames = new TreeSet<String>();
        
        if ("ALL".equals(cacheName))
        {
            for (String tmpCacheName : cacheManager.getCacheNames())
            {
                Ehcache cache = cacheManager.getEhcache(tmpCacheName);
                
                if (cache != null)
                {
                    cache.flush();
                    
                    cacheNames.add(tmpCacheName);
                }
            }
        }
        else
        {
            Ehcache cache = cacheManager.getEhcache(cacheName);
            
            cache.flush();

            cacheNames.add(cacheName);
        }
        
        return cacheNames;
    }
}
 